Conversation
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughAdds a voucher management feature: admin API for create/update/soft-delete and expiry cleanup, user API to list active vouchers, DTOs and validators, repository/service implementations, DI registrations, and email notification for new vouchers. All operations use ErrorOr for results and UTC-normalized DateTime fields. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Controller as VoucherController
participant Service as VoucherService
participant Repo as VoucherRepository
participant UoW as UnitOfWork
participant DB as Database
participant Email as EmailService
Client->>Controller: POST /api/admin/vouchers (CreateVoucherDto)
Controller->>Service: CreateVoucherAsync(dto, token)
Service->>Repo: Add(Voucher entity)
Repo->>DB: INSERT Voucher
DB-->>Repo: OK
Service->>UoW: SaveChangesAsync()
UoW->>DB: COMMIT
DB-->>UoW: OK
Service->>Email: NotifyUserAboutNewVoucherAsync(adminEmail, name, validTo, token)
Email->>Email: SendEmailAsync per user
Email-->>Service: Success
Service-->>Controller: Result.Created
Controller-->>Client: 201 Created
sequenceDiagram
participant Client
participant Controller as UserVoucherController
participant Service as VoucherService
participant UserVoucherRepo as UserVoucherRepository
participant DB as Database
Client->>Controller: GET /api/user/vouchers/user
Controller->>Service: GetAllVoucherForUserAsync(userGuid, token)
Service->>UserVoucherRepo: GetVouchersByUserGuid(userGuid)
UserVoucherRepo->>DB: SELECT ... JOIN Voucher WHERE PublicId = userGuid
DB-->>UserVoucherRepo: Rows
Service->>Service: Filter !IsDeleted && ValidTo > UtcNow, project DTOs
Service-->>Controller: PageResult<ResponseVoucherUserDto>
Controller-->>Client: 200 OK
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (8)
Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.cs (1)
8-11: Remove commented-out interface members.Keep the public service contract focused on implemented operations; track the future CRUD methods in an issue instead of commented stubs.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.cs` around lines 8 - 11, In IVoucherService remove the four commented-out interface members (GetVoucherByGuidAsync, GetAllVouchersAsync, UpdateVoucherAsync, DeleteVoucherAsync) so the public contract only contains implemented methods; delete those commented lines from the IVoucherService interface file and, if you want to track them for later, create an issue/ticket instead of leaving commented stubs in the codebase.Backend/src/ShoeStore.Application/DTOs/VoucherDtos/UpdateVoucherDto.cs (1)
8-14: Keep partial-update fields nullable and audit timestamps server-owned.
MaxPriceDiscount = 0,IsDeleted = false, and client-settableUpdateAtmake it easy to overwrite existing data when a field is simply omitted. Set audit values in the service layer and keep optional update fields nullable.♻️ Suggested DTO adjustment
- public decimal MaxPriceDiscount { get; set; } - public DateTime UpdateAt { get; set; } = DateTime.UtcNow; + public decimal? MaxPriceDiscount { get; set; } public DateTime? ValidFrom { get; set; } public DateTime? ValidTo { get; set; } public int? MaxUsagePerUser { get; set; } public int? TotalQuantity { get; set; } - public bool? IsDeleted { get; set; } = false; + public bool? IsDeleted { get; set; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Backend/src/ShoeStore.Application/DTOs/VoucherDtos/UpdateVoucherDto.cs` around lines 8 - 14, Update the UpdateVoucherDto so optional update fields are nullable and audit timestamps are server-owned: change MaxPriceDiscount to decimal? (nullable) and IsDeleted to bool? (nullable) and remove the client-settable UpdateAt (either remove the property or make it internal/handled only by the service), and remove any default values (e.g., = false or = DateTime.UtcNow) from the DTO; then ensure the service layer (e.g., Voucher update handler) sets UpdateAt = DateTime.UtcNow and applies partial updates only when nullable DTO fields (MaxPriceDiscount, IsDeleted, ValidFrom, ValidTo, MaxUsagePerUser, TotalQuantity) are non-null.Backend/src/ShoeStore.Application/DTOs/VoucherDtos/ResponseVoucherDto.cs (1)
7-10: Avoid shipping an empty public response DTO.If this DTO is returned later, clients will only receive
{}. Either add the voucher response fields now or remove the DTO until an endpoint actually needs it.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Backend/src/ShoeStore.Application/DTOs/VoucherDtos/ResponseVoucherDto.cs` around lines 7 - 10, The ResponseVoucherDto class is empty; either populate it with the actual response properties or remove it until needed. Fix by updating the ResponseVoucherDto DTO to include the voucher response fields used by your API (e.g., properties such as Id, Code, DiscountAmount or Percentage, ValidFrom, ValidTo, IsActive or whatever the domain requires) so clients receive meaningful JSON, or delete ResponseVoucherDto and refactor any controllers/services that reference it to use the correct existing DTO/entity until you define the final response shape.Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherRepository.cs (1)
8-9: These methods are currently unused; if implemented, prefer async materialized results overIQueryable.
GetAllVouchersandGetVoucherByGuidare defined but not called anywhere in the codebase. When they're implemented, follow the established async pattern across other repositories:Task<IReadOnlyList<Voucher>> GetAllVouchersAsync(CancellationToken token)andTask<Voucher?> GetVoucherByGuidAsync(Guid voucherGuid, CancellationToken token). ReturningIQueryabledefers execution, cancellation, and DbContext lifetime handling to callers; materialized results provide cleaner encapsulation and align withICategoryRepository,IColorRepository,ISizeRepository, and similar patterns in the codebase.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherRepository.cs` around lines 8 - 9, The interface IVoucherRepository currently exposes IQueryable methods GetAllVouchers and GetVoucherByGuid; change these to async, materialized signatures to match the repository pattern used elsewhere by replacing GetAllVouchers() with Task<IReadOnlyList<Voucher>> GetAllVouchersAsync(CancellationToken token) and GetVoucherByGuid(Guid voucherGuid) with Task<Voucher?> GetVoucherByGuidAsync(Guid voucherGuid, CancellationToken token), and update implementations to materialize queries with cancellation support (e.g., ToListAsync/FirstOrDefaultAsync) so callers don't handle IQueryable/DbContext lifetime.Backend/src/ShoeStore.Application/Services/VoucherService.cs (2)
44-62: Remove commented-out stubs.These commented
DeleteVoucherAsync/GetAllVouchersAsync/GetVoucherByGuidAsync/UpdateVoucherAsyncblocks are dead code. Track unimplemented methods via an issue/TODO rather than committed commented code; source control preserves history.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Backend/src/ShoeStore.Application/Services/VoucherService.cs` around lines 44 - 62, Remove the commented-out stub methods in VoucherService.cs (the commented DeleteVoucherAsync, GetAllVouchersAsync, GetVoucherByGuidAsync, and UpdateVoucherAsync blocks); delete those commented blocks entirely and, if you need to track the missing implementations, replace them with a single TODO comment or create a ticket/issue and reference its ID in a short TODO line (e.g., "// TODO: implement voucher operations - ISSUE-1234") so history is preserved by source control while keeping the codebase clean.
22-36: Silent defaulting of nullable required fields masks validation failures.
VoucherName ?? string.Empty,Discount ?? 0,TotalQuantity ?? 0,MinOrderPrice ?? 0will happily create a voucher with an empty name or zero discount/quantity if validation is ever bypassed (e.g., validator not wired, or a new code path). SinceCreateVoucherDtomodels these as nullable, the service should either reject nulls explicitly or rely on strongly-typed (non-nullable) DTO properties. Recommended: make required fields non-nullable in the DTO so the compiler/validator enforces presence and this service doesn't need to coalesce.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Backend/src/ShoeStore.Application/Services/VoucherService.cs` around lines 22 - 36, The current object initializer in VoucherService that builds the Voucher from voucherCreateDto silently coalesces nullable fields (voucherCreateDto.VoucherName, Discount, TotalQuantity, MinOrderPrice) to defaults; instead either make those properties non-nullable on CreateVoucherDto so the compiler/validation enforces them, or add explicit null/invalid checks in VoucherService (e.g., in the method that constructs the Voucher) and throw a clear exception (ArgumentException/ValidationException) when VoucherName is null/empty or numeric fields are null/invalid before creating the Voucher instance; locate the initialization in the VoucherService method that references voucherCreateDto and update it to validate and reject nulls rather than using ?? defaults.Backend/src/ShoeStore.Infrastructure/Repositories/VoucherRepository.cs (1)
14-17: Consider returning a single entity for GUID lookup.
GetVoucherByGuidreturnsIQueryable<Voucher>for what is logically a single-entity lookup (PublicId should be unique). Callers have to callFirstOrDefaultAsyncthemselves, and theIQueryableshape leaks EF concerns into the application layer. ConsiderTask<Voucher?> GetVoucherByGuidAsync(Guid voucherGuid, CancellationToken token)instead. Also, style-wiseAsNoTracking()is typically applied beforeWhere(...)(functionally equivalent here but more conventional).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Backend/src/ShoeStore.Infrastructure/Repositories/VoucherRepository.cs` around lines 14 - 17, Change the repository method GetVoucherByGuid to return a single entity asynchronously: replace the IQueryable-returning GetVoucherByGuid with Task<Voucher?> GetVoucherByGuidAsync(Guid voucherGuid, CancellationToken cancellationToken) that queries context.Vouchers.AsNoTracking().Where(v => v.PublicId == voucherGuid).FirstOrDefaultAsync(cancellationToken); update any callers to await the new async method and remove any .FirstOrDefaultAsync usage there; keep AsNoTracking() applied before Where for style/convention and ensure you include the CancellationToken in the EF Core call.Backend/src/ShoeStore.Api/Controllers/VoucherController.cs (1)
47-53:Createdresponse lacks a resource location and identifier.
Created("", new { message = ... })emits a 201 with an emptyLocationheader and does not return the newly created voucher's identifier (e.g.,PublicId). Consumers cannot discover the created resource. Consider returning the new voucher'sPublicIdfromCreateVoucherAsync(e.g.,ErrorOr<Guid>or a small result DTO) and usingCreatedAtAction(nameof(GetVoucherByGuid), new { id = ... }, ...)once a GET endpoint exists, or at minimum include thePublicIdin the response body.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Backend/src/ShoeStore.Api/Controllers/VoucherController.cs` around lines 47 - 53, The Created response in VoucherController currently uses Created("", ...) which sends an empty Location header and omits the created voucher identifier; update the CreateVoucherAsync flow (or the method returning result) to return the new voucher PublicId (e.g., change CreateVoucherAsync to return ErrorOr<Guid> or a DTO containing PublicId), then replace Created("", ...) with CreatedAtAction(nameof(GetVoucherByGuid), new { id = publicId }, new { message = "Voucher created successfully", publicId }) or at minimum include publicId in the response body so consumers can discover the resource.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Backend/src/ShoeStore.Application/DTOs/VoucherDtos/CreateVoucherDto.cs`:
- Around line 13-24: The setters for ValidFrom and ValidTo currently use
DateTime.SpecifyKind which only labels the value as UTC without converting the
instant; update the setters in CreateVoucherDto (properties ValidFrom/ValidTo,
backing fields _validFrom/_validTo) to convert incoming non-null DateTime values
to UTC using ToUniversalTime() (e.g., set _validFrom = value.HasValue ?
value.Value.ToUniversalTime() : null) so timestamps are actually normalized to
UTC while preserving null handling.
In `@Backend/src/ShoeStore.Application/Services/VoucherService.cs`:
- Around line 27-28: The CreateVoucherDtoValidation is missing enum validation,
so unvalidated int values are being cast into enums in VoucherService when
assigning VoucherScope and DiscountType; update CreateVoucherDtoValidation to
add IsInEnum() rules for the VoucherScope and DiscountType properties (e.g.,
RuleFor(x => x.VoucherScope).IsInEnum().WithMessage(...); RuleFor(x =>
x.DiscountType).IsInEnum().WithMessage(...)) to ensure invalid ints are rejected
before VoucherService performs the casts.
In
`@Backend/src/ShoeStore.Application/Validations/VoucherValidation/CreateVoucherDtoValidation.cs`:
- Around line 10-19: Add validations for enum and numeric constraints: ensure
VoucherScope and DiscountType use IsInEnum() (e.g., RuleFor(x =>
x.VoucherScope).IsInEnum().WithMessage(...), RuleFor(x =>
x.DiscountType).IsInEnum().WithMessage(...)); add a conditional rule that when
DiscountType == DiscountType.Percentage the Discount value is <= 100 (RuleFor(x
=> x.Discount).LessThanOrEqualTo(100).When(x => x.DiscountType ==
DiscountType.Percentage).WithMessage(...)); and enforce non-negative constraints
for MaxPriceDiscount, MinOrderPrice and MaxUsagePerUser (e.g., RuleFor(x =>
x.MaxPriceDiscount).GreaterThanOrEqualTo(0)..., RuleFor(x =>
x.MinOrderPrice).GreaterThanOrEqualTo(0)..., RuleFor(x =>
x.MaxUsagePerUser).GreaterThanOrEqualTo(0)...). Use the
CreateVoucherDtoValidation class and the property names VoucherScope,
DiscountType, Discount, MaxPriceDiscount, MinOrderPrice, and MaxUsagePerUser to
locate where to add these rules.
---
Nitpick comments:
In `@Backend/src/ShoeStore.Api/Controllers/VoucherController.cs`:
- Around line 47-53: The Created response in VoucherController currently uses
Created("", ...) which sends an empty Location header and omits the created
voucher identifier; update the CreateVoucherAsync flow (or the method returning
result) to return the new voucher PublicId (e.g., change CreateVoucherAsync to
return ErrorOr<Guid> or a DTO containing PublicId), then replace Created("",
...) with CreatedAtAction(nameof(GetVoucherByGuid), new { id = publicId }, new {
message = "Voucher created successfully", publicId }) or at minimum include
publicId in the response body so consumers can discover the resource.
In `@Backend/src/ShoeStore.Application/DTOs/VoucherDtos/ResponseVoucherDto.cs`:
- Around line 7-10: The ResponseVoucherDto class is empty; either populate it
with the actual response properties or remove it until needed. Fix by updating
the ResponseVoucherDto DTO to include the voucher response fields used by your
API (e.g., properties such as Id, Code, DiscountAmount or Percentage, ValidFrom,
ValidTo, IsActive or whatever the domain requires) so clients receive meaningful
JSON, or delete ResponseVoucherDto and refactor any controllers/services that
reference it to use the correct existing DTO/entity until you define the final
response shape.
In `@Backend/src/ShoeStore.Application/DTOs/VoucherDtos/UpdateVoucherDto.cs`:
- Around line 8-14: Update the UpdateVoucherDto so optional update fields are
nullable and audit timestamps are server-owned: change MaxPriceDiscount to
decimal? (nullable) and IsDeleted to bool? (nullable) and remove the
client-settable UpdateAt (either remove the property or make it internal/handled
only by the service), and remove any default values (e.g., = false or =
DateTime.UtcNow) from the DTO; then ensure the service layer (e.g., Voucher
update handler) sets UpdateAt = DateTime.UtcNow and applies partial updates only
when nullable DTO fields (MaxPriceDiscount, IsDeleted, ValidFrom, ValidTo,
MaxUsagePerUser, TotalQuantity) are non-null.
In
`@Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherRepository.cs`:
- Around line 8-9: The interface IVoucherRepository currently exposes IQueryable
methods GetAllVouchers and GetVoucherByGuid; change these to async, materialized
signatures to match the repository pattern used elsewhere by replacing
GetAllVouchers() with Task<IReadOnlyList<Voucher>>
GetAllVouchersAsync(CancellationToken token) and GetVoucherByGuid(Guid
voucherGuid) with Task<Voucher?> GetVoucherByGuidAsync(Guid voucherGuid,
CancellationToken token), and update implementations to materialize queries with
cancellation support (e.g., ToListAsync/FirstOrDefaultAsync) so callers don't
handle IQueryable/DbContext lifetime.
In
`@Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.cs`:
- Around line 8-11: In IVoucherService remove the four commented-out interface
members (GetVoucherByGuidAsync, GetAllVouchersAsync, UpdateVoucherAsync,
DeleteVoucherAsync) so the public contract only contains implemented methods;
delete those commented lines from the IVoucherService interface file and, if you
want to track them for later, create an issue/ticket instead of leaving
commented stubs in the codebase.
In `@Backend/src/ShoeStore.Application/Services/VoucherService.cs`:
- Around line 44-62: Remove the commented-out stub methods in VoucherService.cs
(the commented DeleteVoucherAsync, GetAllVouchersAsync, GetVoucherByGuidAsync,
and UpdateVoucherAsync blocks); delete those commented blocks entirely and, if
you need to track the missing implementations, replace them with a single TODO
comment or create a ticket/issue and reference its ID in a short TODO line
(e.g., "// TODO: implement voucher operations - ISSUE-1234") so history is
preserved by source control while keeping the codebase clean.
- Around line 22-36: The current object initializer in VoucherService that
builds the Voucher from voucherCreateDto silently coalesces nullable fields
(voucherCreateDto.VoucherName, Discount, TotalQuantity, MinOrderPrice) to
defaults; instead either make those properties non-nullable on CreateVoucherDto
so the compiler/validation enforces them, or add explicit null/invalid checks in
VoucherService (e.g., in the method that constructs the Voucher) and throw a
clear exception (ArgumentException/ValidationException) when VoucherName is
null/empty or numeric fields are null/invalid before creating the Voucher
instance; locate the initialization in the VoucherService method that references
voucherCreateDto and update it to validate and reject nulls rather than using ??
defaults.
In `@Backend/src/ShoeStore.Infrastructure/Repositories/VoucherRepository.cs`:
- Around line 14-17: Change the repository method GetVoucherByGuid to return a
single entity asynchronously: replace the IQueryable-returning GetVoucherByGuid
with Task<Voucher?> GetVoucherByGuidAsync(Guid voucherGuid, CancellationToken
cancellationToken) that queries context.Vouchers.AsNoTracking().Where(v =>
v.PublicId == voucherGuid).FirstOrDefaultAsync(cancellationToken); update any
callers to await the new async method and remove any .FirstOrDefaultAsync usage
there; keep AsNoTracking() applied before Where for style/convention and ensure
you include the CancellationToken in the EF Core call.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: fde2b7b4-ad97-42e0-9f47-77eb870f4d83
📒 Files selected for processing (11)
Backend/src/ShoeStore.Api/Controllers/VoucherController.csBackend/src/ShoeStore.Application/DTOs/VoucherDtos/CreateVoucherDto.csBackend/src/ShoeStore.Application/DTOs/VoucherDtos/ResponseVoucherDto.csBackend/src/ShoeStore.Application/DTOs/VoucherDtos/UpdateVoucherDto.csBackend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherRepository.csBackend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.csBackend/src/ShoeStore.Application/Services/VoucherService.csBackend/src/ShoeStore.Application/ShoeStore.Application.csprojBackend/src/ShoeStore.Application/Validations/VoucherValidation/CreateVoucherDtoValidation.csBackend/src/ShoeStore.Infrastructure/DependencyInjection/DependencyInjection.csBackend/src/ShoeStore.Infrastructure/Repositories/VoucherRepository.cs
| public DateTime? ValidFrom | ||
| { | ||
| get => _validFrom; | ||
| set => _validFrom = value.HasValue ? DateTime.SpecifyKind(value.Value, DateTimeKind.Utc) : null; | ||
| } | ||
|
|
||
| private DateTime? _validTo; | ||
| public DateTime? ValidTo | ||
| { | ||
| get => _validTo; | ||
| set => _validTo = value.HasValue ? DateTime.SpecifyKind(value.Value, DateTimeKind.Utc) : null; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Microsoft .NET DateTime.SpecifyKind documentation: does SpecifyKind convert the DateTime value to UTC or only set the Kind property?
💡 Result:
DateTime.SpecifyKind does not convert the DateTime value to UTC. It only sets the Kind property while keeping the same number of ticks (time value) unchanged. The official documentation explicitly states: "The returned DateTime value does not represent the same instant in time as the value parameter, and SpecifyKind is not a time zone conversion method. Instead, it leaves the time specified by the value parameter unchanged, and sets the Kind property to kind." For time zone conversions, use methods like ToUniversalTime or ToLocalTime.
Citations:
- 1: https://learn.microsoft.com/en-us/dotnet/api/system.datetime.specifykind?view=net-10.0
- 2: https://msdn.microsoft.com/en-us/library/system.datetime.specifykind
- 3: https://msdn.microsoft.com/library/system.datetime.specifykind
🏁 Script executed:
# First, locate and inspect the CreateVoucherDto.cs file
find . -name "CreateVoucherDto.cs" -type fRepository: tqha1011/ShoeStore
Length of output: 134
🏁 Script executed:
# Search for DateTime.SpecifyKind usage across the codebase
rg "SpecifyKind" -t cs -C 3Repository: tqha1011/ShoeStore
Length of output: 5521
🏁 Script executed:
# Search for similar DateTime assignment patterns in DTOs
rg "DateTime\.SpecifyKind|DateTimeKind\.Utc" -t cs --glob "**/*Dto.cs"Repository: tqha1011/ShoeStore
Length of output: 419
Use ToUniversalTime() to convert non-UTC times, not SpecifyKind().
Lines 16 and 23 use DateTime.SpecifyKind(), which only sets metadata without converting the timestamp. If clients send local or offset-based times, they get relabeled as UTC without any actual conversion, causing vouchers to expire at the wrong instant.
🛠️ Proper UTC normalization
+ private static DateTime? NormalizeUtc(DateTime? value)
+ {
+ if (!value.HasValue)
+ {
+ return null;
+ }
+
+ return value.Value.Kind == DateTimeKind.Unspecified
+ ? DateTime.SpecifyKind(value.Value, DateTimeKind.Utc)
+ : value.Value.ToUniversalTime();
+ }
+
private DateTime? _validFrom;
public DateTime? ValidFrom
{
get => _validFrom;
- set => _validFrom = value.HasValue ? DateTime.SpecifyKind(value.Value, DateTimeKind.Utc) : null;
+ set => _validFrom = NormalizeUtc(value);
}
private DateTime? _validTo;
public DateTime? ValidTo
{
get => _validTo;
- set => _validTo = value.HasValue ? DateTime.SpecifyKind(value.Value, DateTimeKind.Utc) : null;
+ set => _validTo = NormalizeUtc(value);
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Backend/src/ShoeStore.Application/DTOs/VoucherDtos/CreateVoucherDto.cs`
around lines 13 - 24, The setters for ValidFrom and ValidTo currently use
DateTime.SpecifyKind which only labels the value as UTC without converting the
instant; update the setters in CreateVoucherDto (properties ValidFrom/ValidTo,
backing fields _validFrom/_validTo) to convert incoming non-null DateTime values
to UTC using ToUniversalTime() (e.g., set _validFrom = value.HasValue ?
value.Value.ToUniversalTime() : null) so timestamps are actually normalized to
UTC while preserving null handling.
| VoucherScope = (VoucherScope)voucherCreateDto.VoucherScope, | ||
| DiscountType = (DiscountType)voucherCreateDto.DiscountType, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
fd CreateVoucherDtoValidation.cs --exec cat {}
fd CreateVoucherDto.cs --exec cat {}Repository: tqha1011/ShoeStore
Length of output: 2141
Unvalidated int→enum casts persist invalid enum values.
Verified: CreateVoucherDtoValidation contains no validation for VoucherScope or DiscountType. Both fields lack IsInEnum() rules, allowing any int to silently cast to an enum on lines 27–28, corrupting voucher records.
Add to CreateVoucherDtoValidation:
RuleFor(x => x.VoucherScope)
.IsInEnum().WithMessage("Invalid VoucherScope value.");
RuleFor(x => x.DiscountType)
.IsInEnum().WithMessage("Invalid DiscountType value.");🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Backend/src/ShoeStore.Application/Services/VoucherService.cs` around lines 27
- 28, The CreateVoucherDtoValidation is missing enum validation, so unvalidated
int values are being cast into enums in VoucherService when assigning
VoucherScope and DiscountType; update CreateVoucherDtoValidation to add
IsInEnum() rules for the VoucherScope and DiscountType properties (e.g.,
RuleFor(x => x.VoucherScope).IsInEnum().WithMessage(...); RuleFor(x =>
x.DiscountType).IsInEnum().WithMessage(...)) to ensure invalid ints are rejected
before VoucherService performs the casts.
| RuleFor(x => x.VoucherName) | ||
| .NotEmpty().WithMessage("Voucher name is required") | ||
| .MaximumLength(100).WithMessage("Voucher name must not exceed 100 characters"); | ||
|
|
||
| RuleFor(x => x.Discount) | ||
| .NotNull().WithMessage("Discount is required") | ||
| .GreaterThanOrEqualTo(0).WithMessage("Discount must be greater than or equal to 0"); | ||
|
|
||
| RuleFor(x => x.TotalQuantity) | ||
| .GreaterThan(0).WithMessage("Total quantity must be greater than 0"); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Find the domain/entity definitions and usages for voucher scope/type/value fields.
# Expected: Use the discovered allowed values to replace the placeholder Must(...) checks above.
rg -n -C3 --type=cs '\b(VoucherScope|DiscountType|MaxPriceDiscount|MinOrderPrice|MaxUsagePerUser)\b'Repository: tqha1011/ShoeStore
Length of output: 50374
🏁 Script executed:
fd -n "CreateVoucherDto" --type fRepository: tqha1011/ShoeStore
Length of output: 292
🏁 Script executed:
fd "CreateVoucherDto" --type fRepository: tqha1011/ShoeStore
Length of output: 226
🏁 Script executed:
cat -n "Backend/src/ShoeStore.Application/DTOs/VoucherDtos/CreateVoucherDto.cs"Repository: tqha1011/ShoeStore
Length of output: 1334
🏁 Script executed:
fd "Voucher" --type f | grep -E "(Entity|Domain|Model)" | head -20Repository: tqha1011/ShoeStore
Length of output: 268
🏁 Script executed:
cat -n "Backend/src/ShoeStore.Domain/Entities/Voucher.cs"Repository: tqha1011/ShoeStore
Length of output: 1532
🏁 Script executed:
cat -n "Backend/src/ShoeStore.Domain/Enum/VoucherScope.cs"Repository: tqha1011/ShoeStore
Length of output: 207
🏁 Script executed:
fd "DiscountType" --type fRepository: tqha1011/ShoeStore
Length of output: 111
🏁 Script executed:
cat -n "Backend/src/ShoeStore.Domain/Enum/DiscountType.cs"Repository: tqha1011/ShoeStore
Length of output: 213
🏁 Script executed:
cat -n "Backend/src/ShoeStore.Application/Validations/VoucherValidation/CreateVoucherDtoValidation.cs"Repository: tqha1011/ShoeStore
Length of output: 1253
🏁 Script executed:
cat -n "Backend/src/ShoeStore.Application/Validations/VoucherValidation/CreateVoucherDtoValidation.cs" | head -30Repository: tqha1011/ShoeStore
Length of output: 1253
Add missing validation rules for voucher enum fields and economic constraints.
The validation is incomplete. VoucherScope and DiscountType accept arbitrary integers, percentage discounts can exceed 100%, and negative values are allowed for MaxPriceDiscount, MinOrderPrice, and MaxUsagePerUser. These should be rejected at the DTO boundary.
🛡️ Add missing validation rules
+ RuleFor(x => x.VoucherScope)
+ .Must(value => value is 1 or 2)
+ .WithMessage("Voucher scope must be 1 (Product) or 2 (Shipping)");
+
+ RuleFor(x => x.DiscountType)
+ .Must(value => value is 1 or 2)
+ .WithMessage("Discount type must be 1 (FixedAmount) or 2 (Percentage)");
+
RuleFor(x => x.Discount)
.NotNull().WithMessage("Discount is required")
.GreaterThanOrEqualTo(0).WithMessage("Discount must be greater than or equal to 0");
+
+ RuleFor(x => x.Discount)
+ .LessThanOrEqualTo(100).WithMessage("Percentage discount must not exceed 100")
+ .When(x => x.DiscountType == 2);
+
+ RuleFor(x => x.MaxPriceDiscount)
+ .GreaterThanOrEqualTo(0).WithMessage("Max price discount must be greater than or equal to 0");
+
+ RuleFor(x => x.MaxUsagePerUser)
+ .GreaterThan(0).WithMessage("Max usage per user must be greater than 0")
+ .When(x => x.MaxUsagePerUser.HasValue);
+
+ RuleFor(x => x.MinOrderPrice)
+ .GreaterThanOrEqualTo(0).WithMessage("Minimum order price must be greater than or equal to 0")
+ .When(x => x.MinOrderPrice.HasValue);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@Backend/src/ShoeStore.Application/Validations/VoucherValidation/CreateVoucherDtoValidation.cs`
around lines 10 - 19, Add validations for enum and numeric constraints: ensure
VoucherScope and DiscountType use IsInEnum() (e.g., RuleFor(x =>
x.VoucherScope).IsInEnum().WithMessage(...), RuleFor(x =>
x.DiscountType).IsInEnum().WithMessage(...)); add a conditional rule that when
DiscountType == DiscountType.Percentage the Discount value is <= 100 (RuleFor(x
=> x.Discount).LessThanOrEqualTo(100).When(x => x.DiscountType ==
DiscountType.Percentage).WithMessage(...)); and enforce non-negative constraints
for MaxPriceDiscount, MinOrderPrice and MaxUsagePerUser (e.g., RuleFor(x =>
x.MaxPriceDiscount).GreaterThanOrEqualTo(0)..., RuleFor(x =>
x.MinOrderPrice).GreaterThanOrEqualTo(0)..., RuleFor(x =>
x.MaxUsagePerUser).GreaterThanOrEqualTo(0)...). Use the
CreateVoucherDtoValidation class and the property names VoucherScope,
DiscountType, Discount, MaxPriceDiscount, MinOrderPrice, and MaxUsagePerUser to
locate where to add these rules.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
Backend/src/ShoeStore.Api/Controllers/VoucherController.cs (1)
48-69: Avoid mapping all non-404 service errors to 400.Both endpoints collapse service failures into
BadRequest, which hides conflicts or unexpected server errors and diverges from the existing controller patterns that map byErrorType/code.Proposed direction
return result.Match<IActionResult>( _ => Created("", new { message = "Voucher created successfully" }), - errors => BadRequest(new - { - message = "Failed to create voucher", - details = errors - })); + errors => errors[0].Type switch + { + ErrorType.Conflict => Conflict(new { message = "Failed to create voucher", details = errors }), + ErrorType.Validation => BadRequest(new { message = "Failed to create voucher", details = errors }), + _ => StatusCode(StatusCodes.Status500InternalServerError, + new { message = "Failed to create voucher", details = errors }) + });Apply the same pattern to
UpdateVoucher, keepingErrorType.NotFound => NotFound(...).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Backend/src/ShoeStore.Api/Controllers/VoucherController.cs` around lines 48 - 69, The UpdateVoucher endpoint currently maps all non-NotFound errors to BadRequest; change the error handling for voucherService.UpdateVoucherAsync in UpdateVoucher (and mirror CreateVoucher if applicable) to map ErrorType values explicitly (e.g., if any error.Type == ErrorType.NotFound return NotFound(...); if any error.Type == ErrorType.Conflict return Conflict(...); otherwise return StatusCode(500, ...) or BadRequest depending on ErrorType) so conflicts and server errors are not misclassified; use the existing errors collection and ErrorType enum to decide the correct IActionResult and include error details where appropriate.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Backend/src/ShoeStore.Api/Controllers/VoucherController.cs`:
- Around line 14-17: The controller has its [Authorize(Roles = "Admin")]
attribute commented out, exposing admin voucher endpoints; re-enable the
attribute by restoring [Authorize(Roles = "Admin")] on the VoucherController
(class VoucherController) or alternatively apply it to the specific mutation
actions (e.g., Create/Update methods in VoucherController) so that only users in
the Admin role can call the admin voucher endpoints.
In `@Backend/src/ShoeStore.Application/Services/VoucherService.cs`:
- Around line 64-78: The update logic in VoucherService.cs is overwriting
existing voucher fields with DTO defaults because fields like VoucherScope,
DiscountType, and MaxPriceDiscount are non-nullable on voucherUpdateDto and are
always written; also Discount is never updated. Make the corresponding
properties on the update DTO nullable (e.g., int? VoucherScope, int?
DiscountType, decimal? MaxPriceDiscount, and include a nullable Discount
property if intended), then change VoucherService.Update mapping to only assign
when the DTO value.HasValue or DTO string is non-null (e.g., check
voucherUpdateDto.VoucherScope.HasValue before casting and assigning to
voucher.VoucherScope), and add mapping for Discount from voucherUpdateDto when
provided; this preserves existing values on partial updates.
- Around line 52-54: In UpdateVoucherAsync, the database read call using
repository.GetVoucherByGuid(...).FirstOrDefaultAsync() does not pass the
CancellationToken parameter, so the read cannot be cancelled; update the call to
pass the provided CancellationToken token into FirstOrDefaultAsync(token) (or
the appropriate overload) so the cancellation token is honored during the query
before you later call SaveChangesAsync(token).
---
Nitpick comments:
In `@Backend/src/ShoeStore.Api/Controllers/VoucherController.cs`:
- Around line 48-69: The UpdateVoucher endpoint currently maps all non-NotFound
errors to BadRequest; change the error handling for
voucherService.UpdateVoucherAsync in UpdateVoucher (and mirror CreateVoucher if
applicable) to map ErrorType values explicitly (e.g., if any error.Type ==
ErrorType.NotFound return NotFound(...); if any error.Type == ErrorType.Conflict
return Conflict(...); otherwise return StatusCode(500, ...) or BadRequest
depending on ErrorType) so conflicts and server errors are not misclassified;
use the existing errors collection and ErrorType enum to decide the correct
IActionResult and include error details where appropriate.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 1bea080a-5a90-4a81-8bba-d018b0f15727
📒 Files selected for processing (4)
Backend/src/ShoeStore.Api/Controllers/VoucherController.csBackend/src/ShoeStore.Application/DTOs/VoucherDtos/UpdateVoucherDto.csBackend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.csBackend/src/ShoeStore.Application/Services/VoucherService.cs
🚧 Files skipped from review as they are similar to previous changes (2)
- Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.cs
- Backend/src/ShoeStore.Application/DTOs/VoucherDtos/UpdateVoucherDto.cs
| var voucher = await repository | ||
| .GetVoucherByGuid(voucherGuid) | ||
| .FirstOrDefaultAsync(); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify no-token EF async query calls in C# files.
# Expected: voucher query uses FirstOrDefaultAsync(token) or an equivalent CancellationToken overload.
rg -nP --type cs -C2 '\.FirstOrDefaultAsync\s*\(\s*\)'Repository: tqha1011/ShoeStore
Length of output: 545
🏁 Script executed:
cat -n Backend/src/ShoeStore.Application/Services/VoucherService.cs | head -100Repository: tqha1011/ShoeStore
Length of output: 3990
🏁 Script executed:
rg -n 'FirstOrDefaultAsync\s*\(\s*\)' Backend/src/ShoeStore.Application/Services/VoucherService.csRepository: tqha1011/ShoeStore
Length of output: 104
Pass the cancellation token to the database query.
The UpdateVoucherAsync method accepts a CancellationToken token parameter but doesn't pass it to FirstOrDefaultAsync() on line 54. Without the token, the database read operation cannot be cancelled, even though the save operation on line 82 correctly uses it.
Proposed fix
var voucher = await repository
.GetVoucherByGuid(voucherGuid)
- .FirstOrDefaultAsync();
+ .FirstOrDefaultAsync(token);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Backend/src/ShoeStore.Application/Services/VoucherService.cs` around lines 52
- 54, In UpdateVoucherAsync, the database read call using
repository.GetVoucherByGuid(...).FirstOrDefaultAsync() does not pass the
CancellationToken parameter, so the read cannot be cancelled; update the call to
pass the provided CancellationToken token into FirstOrDefaultAsync(token) (or
the appropriate overload) so the cancellation token is honored during the query
before you later call SaveChangesAsync(token).
| [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] | ||
| [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] | ||
| [HttpGet("user/{userGuid}")] | ||
| public async Task<IActionResult> GetVouchersForUser(Guid userGuid, CancellationToken token) |
There was a problem hiding this comment.
cái này k cần phải là tham số, lấy từ jwt token dùng find first value
|
|
||
| namespace ShoeStore.Application.Services | ||
| { | ||
| public class VoucherService : IVoucherService, IUserVoucherService |
| }) | ||
| .ToListAsync(token); | ||
|
|
||
| if(vouchers == null || !vouchers.Any()) |
There was a problem hiding this comment.
Actionable comments posted: 11
♻️ Duplicate comments (2)
Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs (1)
27-27:⚠️ Potential issue | 🟡 MinorStale XML doc —
userGuidparameter no longer exists.The
<param name="userGuid">doc references a parameter that was removed when the GUID was sourced from the JWT claim. Drop the line so the doc matches the current signature.- /// <param name="userGuid">The unique identifier (GUID) of the user whose vouchers are being retrieved.</param> /// <param name="token">Cancellation token for the request.</param>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs` at line 27, Remove the stale XML doc line that references the removed parameter "userGuid" in the UserVoucherController XML comments so the documentation matches the current method signature; update the docblock for the controller method that retrieves vouchers (in class UserVoucherController, e.g., the Get.../Retrieve... method that now reads the GUID from JWT claims) by deleting the <param name="userGuid">...</param> entry and ensure remaining <param> tags correspond to actual parameters.Backend/src/ShoeStore.Application/Services/VoucherService.cs (1)
58-61:⚠️ Potential issue | 🟡 MinorPass
CancellationTokentoFirstOrDefaultAsync.The method receives a
CancellationToken tokenbut the database read is not cancellable. Pass the token through:var voucher = await voucherRepository .GetVoucherByGuid(voucherGuid) .Where(v => !v.IsDeleted) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(token);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Backend/src/ShoeStore.Application/Services/VoucherService.cs` around lines 58 - 61, The DB read in VoucherService is not using the provided CancellationToken; update the call chain where voucherRepository.GetVoucherByGuid(voucherGuid).Where(...).FirstOrDefaultAsync() to pass the token into FirstOrDefaultAsync so the query is cancellable (keep references to voucherRepository, GetVoucherByGuid, the Where predicate on v => !v.IsDeleted, and FirstOrDefaultAsync and use the method's CancellationToken parameter named token).
🧹 Nitpick comments (6)
Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs (1)
14-14: Stale<param>reference foruserVoucherServicein the class XML doc.The class summary documents
userVoucherServiceas a parameter, but<param>tags on a class doc don't bind to primary-constructor parameters and will not render meaningfully. Either move this description into the constructor or drop the<param>element to silence the doc warning.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs` at line 14, The class XML doc for UserVoucherController contains a stale <param name="userVoucherService"> entry which is invalid on a class; remove that <param> tag from the class summary or relocate its description into the XML doc for the constructor (the UserVoucherController constructor that accepts IUserVoucherService userVoucherService) so the parameter documentation is attached to the actual constructor parameter instead of the class.Backend/src/ShoeStore.Infrastructure/Repositories/UserVoucherRepository.cs (1)
9-18: AddAsNoTracking()for read-only query paths.Both methods feed into read-only projections (e.g.,
VoucherService.GetAllVoucherForUserAsyncprojects toResponseVoucherUserDto), so EF's change tracker is unnecessary overhead. Mirroring the pattern used inUserRepository.GetAllUsers()would avoid materializing tracked entities for every query consumer:public IQueryable<UserVoucher> GetAllVouchers() { - return context.UserVouchers; + return context.UserVouchers.AsNoTracking(); } public IQueryable<UserVoucher> GetVouchersByUserGuid(Guid userGuid) { return context.UserVouchers + .AsNoTracking() .Where(uv => uv.User != null && uv.User.PublicId == userGuid); }If any caller does need to track results, they can opt back in with
.AsTracking().🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Backend/src/ShoeStore.Infrastructure/Repositories/UserVoucherRepository.cs` around lines 9 - 18, The queries in GetAllVouchers and GetVouchersByUserGuid currently return tracked entities; change them to use AsNoTracking() on context.UserVouchers to avoid unnecessary EF change tracking for read-only projection paths (keep the same Where predicate for GetVouchersByUserGuid) and note callers can opt into tracking later with .AsTracking() if needed.Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.cs (2)
11-12: Ambiguous method names — clarify intent of the two admin getters.
GetVoucherForAdminAsyncandGetAllVouchersAsynchave identical signatures and return types, so the difference between them is not discoverable from the API. Consider renaming to reflect intent (e.g.,GetActiveVouchersForAdminAsyncvsGetAllVouchersIncludingDeletedAsync) or merging into a single method with a filter parameter.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.cs` around lines 11 - 12, The two methods on IVoucherService—GetVoucherForAdminAsync and GetAllVouchersAsync—are ambiguous because they share the same signature and return type; either rename them to convey intent (for example, GetActiveVouchersForAdminAsync vs GetAllVouchersIncludingDeletedAsync) or consolidate into a single method (e.g., GetVouchersForAdminAsync) that accepts a filter parameter/enum (e.g., includeDeleted, statusFilter) to disambiguate behavior; update the interface method name(s) and any implementing classes and callers (services/repositories/tests) to match the chosen approach so intent is clear throughout the codebase.
19-19: Consider sourcingadminEmailfrom configuration rather than the call site.Passing
adminEmailas a parameter to a notification method on the service interface couples callers to admin identity. Typically the admin/sender email is a deployment concern that the service should resolve from configuration (e.g.,IOptions<EmailSettings>), not something the controller/caller supplies.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.cs` at line 19, The interface method NotifyUserAboutNewVoucherAsync currently takes adminEmail which couples callers to admin identity; change the IVoucherService signature to remove the adminEmail parameter and instead have the concrete implementation resolve the sender from configuration (inject IOptions<EmailSettings> or a typed EmailSettings instance into the implementing class), update the implementation of NotifyUserAboutNewVoucherAsync to read the configured admin/sender email internally, and update all callers (controllers/services) to stop passing adminEmail; also update DI registration to ensure EmailSettings are bound and available to the service implementation.Backend/src/ShoeStore.Infrastructure/Repositories/UserRepository.cs (1)
27-30: UnboundedGetAllUsers()for notification flow — consider scaling.This method materializes every user when consumed by
VoucherService.NotifyUserAboutNewVoucherAsync. As the user table grows, sending notifications inline on the create-voucher request thread will increasingly degrade latency and risk timeouts. Two thoughts:
- The repo method itself is fine (it returns
IQueryable, deferred), but callers should page/stream rather than enumerate the full set in memory.- Email-fanout for new vouchers is better suited to a background job/queue than the create endpoint.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Backend/src/ShoeStore.Infrastructure/Repositories/UserRepository.cs` around lines 27 - 30, GetAllUsers currently returns an unbounded IQueryable that VoucherService.NotifyUserAboutNewVoucherAsync consumes, causing full-table enumeration during voucher creation; instead, modify the flow so the repository exposes a paged or streaming API (e.g., Add a method like GetUsersPaged(pageSize, pageToken) or GetUsersAsAsyncEnumerable()) and change VoucherService.NotifyUserAboutNewVoucherAsync to enqueue a notification job/message (with voucher id and notification metadata) to a background queue rather than sending emails inline; implement a background worker/consumer that pulls the job and iterates users in pages or via IAsyncEnumerable to send emails in bounded batches with retry/backoff.Backend/src/ShoeStore.Api/Controllers/VoucherController.cs (1)
200-256:GET /andGET /allare functionally identical — confirm intent.
GetVouchersForAdminandGetAllVouchersboth delegate to service methods that have identical implementations inVoucherService.cs(GetVoucherForAdminAsynclines 165–196 andGetAllVouchersAsynclines 131–163 are copy-pasted: same query, same projection, same filters). Either one route is intended to be paginated/filtered (and the duplication is a placeholder), or one of the two endpoints/service methods should be removed to avoid a maintenance hazard.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Backend/src/ShoeStore.Api/Controllers/VoucherController.cs` around lines 200 - 256, GetVouchersForAdmin and GetAllVouchers (and their service counterparts GetVoucherForAdminAsync and GetAllVouchersAsync) are duplicated; confirm intended behavior and either remove one pair or make them different: if one should be paginated/filtered implement pagination/filter params and update GetAllVouchersAsync (or GetVoucherForAdminAsync) to accept page/filters and adjust the controller signature and [ProducesResponseType] docs accordingly; if one is redundant delete the unused controller action and its service method, update any callers/tests and OpenAPI annotations, and consolidate shared query logic into a single private/shared method in VoucherService for reuse.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs`:
- Around line 41-46: Replace the LINQ claim lookup and Guid.Parse with the
idiomatic User.FindFirstValue(ClaimTypes.NameIdentifier) and Guid.TryParse to
avoid throwing FormatException; retrieve the claim value into userGuidString
using FindFirstValue, call Guid.TryParse(userGuidString, out var userGuid) and
if it returns false return Unauthorized (or BadRequest per policy), otherwise
call userVoucherService.GetAllVoucherForUserAsync(userGuid, token) as before.
Ensure you reference the symbols User.FindFirstValue, Guid.TryParse,
userGuidString, userGuid, and userVoucherService.GetAllVoucherForUserAsync when
making the change.
In `@Backend/src/ShoeStore.Api/Controllers/VoucherController.cs`:
- Line 52: The current fallback of var adminEmail =
User.FindFirstValue(ClaimTypes.Email) ?? "[email protected]" in
VoucherController is unsafe; replace this by validating the presence of the
Email claim and returning an appropriate error (e.g., 400/401) if missing, or
read a configured sender address instead of a hardcoded placeholder. Locate the
adminEmail assignment in the VoucherController, remove the "[email protected]"
default, and implement either: (a) a guard that checks
User.FindFirstValue(ClaimTypes.Email) and returns BadRequest/Unauthorized with a
clear message if null/empty, or (b) resolve the sender from configuration
(IConfiguration or options) and use that when the claim is absent, ensuring no
hardcoded reserved address is ever used.
- Around line 49-69: The success branch of result.MatchAsync ignores the outcome
of NotifyUserAboutNewVoucherAsync causing notification failures to be swallowed
or to surface as 500 even though CreateVoucherAsync already persisted the
voucher; update the success branch to await and inspect the ErrorOr result from
NotifyUserAboutNewVoucherAsync (handle both Error and Success cases), return
CreatedAtAction(nameof(GetVouchersForAdmin), ...) or CreatedAtAction with the
new voucher id and include a partial-success warning when notification failed,
and ensure any exceptions from NotifyUserAboutNewVoucherAsync are caught and
translated into a non-failing warning response (or, alternatively, move
notification dispatch out-of-band via an outbox/background job) so HTTP 201
reflects the persisted voucher state rather than the delivery outcome.
- Around line 166-181: The DeleteExpiredVouchers handler currently maps all
errors to BadRequest; change it to inspect the returned errors (from
voucherService.DeleteVoucherExpireAsync) and when errors[0].Code ==
"NO_EXPIRED_VOUCHERS" return NotFound(...) instead of BadRequest, mirroring
other handlers (e.g.,
UpdateVoucher/DeleteVoucher/GetVouchersForAdmin/GetAllVouchers) and add a
corresponding [ProducesResponseType(typeof(object),
StatusCodes.Status404NotFound)] attribute to the DeleteExpiredVouchers action;
ensure other error cases still produce BadRequest as before.
In
`@Backend/src/ShoeStore.Application/DTOs/VoucherDtos/ResponseVoucherUserDto.cs`:
- Around line 9-21: The setters for ValidFrom/ValidTo use DateTime.SpecifyKind
which only relabels Kind and can silently corrupt times; update the setters for
_validFrom and _validTo to ensure the incoming DateTime is converted to UTC
instead of merely changing its Kind — e.g., if value.HasValue, convert
value.Value to UTC (using ToUniversalTime or by checking Kind and calling
ToUniversalTime when Kind != Utc) and then store the converted UTC DateTime;
replace the use of DateTime.SpecifyKind with a proper UTC conversion flow for
both ValidFrom and ValidTo.
In
`@Backend/src/ShoeStore.Application/Interface/VoucherInterface/IUserVoucherService.cs`:
- Line 8: Rename the parameter in the method signature GetAllVoucherForUserAsync
from Guid UserGuid to Guid userGuid to follow C# camelCase conventions and match
other APIs (e.g., GetVouchersByUserGuid); update any implementations and callers
of IUserVoucherService.GetAllVoucherForUserAsync to use the new parameter name
to keep signatures consistent.
- Around line 6-9: Update the VoucherService class to explicitly implement
IUserVoucherService in addition to IVoucherService (so the concrete type
satisfies both interfaces) and register the same implementation for
IUserVoucherService in the DI container where VoucherService is currently
registered; also rename the GetAllVoucherForUserAsync parameter from UserGuid to
camelCase userGuid in the IUserVoucherService signature (and adjust any
implementing method in VoucherService) to follow C# naming conventions and
ensure the UserVoucherController can resolve its IUserVoucherService dependency
at runtime.
In `@Backend/src/ShoeStore.Application/Services/VoucherService.cs`:
- Around line 100-129: GetAllVoucherForUserAsync can NRE on v.Voucher; update
the query from userVoucherRepository.GetVouchersByUserGuid(...) so the predicate
guarantees Voucher is present (e.g., add && v.Voucher != null in the Where chain
or ensure the repository includes/filters Voucher) before dereferencing
v.Voucher fields, then keep the existing projection to ResponseVoucherUserDto;
reference the GetAllVoucherForUserAsync method and the
userVoucherRepository.GetVouchersByUserGuid(...) call and the v.Voucher usages
when applying the change.
- Around line 79-82: The synchronous materialization of the EF query in
VoucherService (variable vouchersToDelete built from
voucherRepository.GetAllVouchers()) blocks the request thread and ignores
cancellation; change the call to use asynchronous EF materialization by awaiting
GetAllVouchers().Where(...).ToListAsync(cancellationToken) (ensure the method
has a CancellationToken parameter or uses the existing token) and update the
method to be async so you await the result and propagate the token to
ToListAsync.
- Line 181: GetVoucherForAdminAsync calls ToListAsync() without the
CancellationToken so the database read can't be cancelled; update the
ToListAsync call in GetVoucherForAdminAsync to pass the method's
cancellationToken (e.g., call ToListAsync(cancellationToken)) matching the
pattern used in GetAllVouchersAsync, ensuring the async query observes client
cancellation.
- Around line 198-246: The loop in NotifyUserAboutNewVoucherAsync currently
returns Error.NotFound because of a redundant/inverted check against user.Email
after fetching users with userRepository.GetAllUsers().Where(u => u.Email !=
adminEmail); remove the entire if (user.Email != adminEmail) { return
Error.NotFound(...); } block, change the admin-email exclusion to a
case-insensitive comparison (e.g., compare using
StringComparer.OrdinalIgnoreCase or ensure the DB collation) when building the
query, and replace the serial await emailService.SendEmailAsync calls with a
strategy that supports concurrency and partial-failure reporting (e.g.,
Task.WhenAll with a bounded concurrency limiter or enqueue jobs to a background
worker) so per-user send failures are collected and returned as aggregated
errors instead of aborting the whole batch.
---
Duplicate comments:
In `@Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs`:
- Line 27: Remove the stale XML doc line that references the removed parameter
"userGuid" in the UserVoucherController XML comments so the documentation
matches the current method signature; update the docblock for the controller
method that retrieves vouchers (in class UserVoucherController, e.g., the
Get.../Retrieve... method that now reads the GUID from JWT claims) by deleting
the <param name="userGuid">...</param> entry and ensure remaining <param> tags
correspond to actual parameters.
In `@Backend/src/ShoeStore.Application/Services/VoucherService.cs`:
- Around line 58-61: The DB read in VoucherService is not using the provided
CancellationToken; update the call chain where
voucherRepository.GetVoucherByGuid(voucherGuid).Where(...).FirstOrDefaultAsync()
to pass the token into FirstOrDefaultAsync so the query is cancellable (keep
references to voucherRepository, GetVoucherByGuid, the Where predicate on v =>
!v.IsDeleted, and FirstOrDefaultAsync and use the method's CancellationToken
parameter named token).
---
Nitpick comments:
In `@Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs`:
- Line 14: The class XML doc for UserVoucherController contains a stale <param
name="userVoucherService"> entry which is invalid on a class; remove that
<param> tag from the class summary or relocate its description into the XML doc
for the constructor (the UserVoucherController constructor that accepts
IUserVoucherService userVoucherService) so the parameter documentation is
attached to the actual constructor parameter instead of the class.
In `@Backend/src/ShoeStore.Api/Controllers/VoucherController.cs`:
- Around line 200-256: GetVouchersForAdmin and GetAllVouchers (and their service
counterparts GetVoucherForAdminAsync and GetAllVouchersAsync) are duplicated;
confirm intended behavior and either remove one pair or make them different: if
one should be paginated/filtered implement pagination/filter params and update
GetAllVouchersAsync (or GetVoucherForAdminAsync) to accept page/filters and
adjust the controller signature and [ProducesResponseType] docs accordingly; if
one is redundant delete the unused controller action and its service method,
update any callers/tests and OpenAPI annotations, and consolidate shared query
logic into a single private/shared method in VoucherService for reuse.
In
`@Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.cs`:
- Around line 11-12: The two methods on IVoucherService—GetVoucherForAdminAsync
and GetAllVouchersAsync—are ambiguous because they share the same signature and
return type; either rename them to convey intent (for example,
GetActiveVouchersForAdminAsync vs GetAllVouchersIncludingDeletedAsync) or
consolidate into a single method (e.g., GetVouchersForAdminAsync) that accepts a
filter parameter/enum (e.g., includeDeleted, statusFilter) to disambiguate
behavior; update the interface method name(s) and any implementing classes and
callers (services/repositories/tests) to match the chosen approach so intent is
clear throughout the codebase.
- Line 19: The interface method NotifyUserAboutNewVoucherAsync currently takes
adminEmail which couples callers to admin identity; change the IVoucherService
signature to remove the adminEmail parameter and instead have the concrete
implementation resolve the sender from configuration (inject
IOptions<EmailSettings> or a typed EmailSettings instance into the implementing
class), update the implementation of NotifyUserAboutNewVoucherAsync to read the
configured admin/sender email internally, and update all callers
(controllers/services) to stop passing adminEmail; also update DI registration
to ensure EmailSettings are bound and available to the service implementation.
In `@Backend/src/ShoeStore.Infrastructure/Repositories/UserRepository.cs`:
- Around line 27-30: GetAllUsers currently returns an unbounded IQueryable that
VoucherService.NotifyUserAboutNewVoucherAsync consumes, causing full-table
enumeration during voucher creation; instead, modify the flow so the repository
exposes a paged or streaming API (e.g., Add a method like
GetUsersPaged(pageSize, pageToken) or GetUsersAsAsyncEnumerable()) and change
VoucherService.NotifyUserAboutNewVoucherAsync to enqueue a notification
job/message (with voucher id and notification metadata) to a background queue
rather than sending emails inline; implement a background worker/consumer that
pulls the job and iterates users in pages or via IAsyncEnumerable to send emails
in bounded batches with retry/backoff.
In `@Backend/src/ShoeStore.Infrastructure/Repositories/UserVoucherRepository.cs`:
- Around line 9-18: The queries in GetAllVouchers and GetVouchersByUserGuid
currently return tracked entities; change them to use AsNoTracking() on
context.UserVouchers to avoid unnecessary EF change tracking for read-only
projection paths (keep the same Where predicate for GetVouchersByUserGuid) and
note callers can opt into tracking later with .AsTracking() if needed.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 8b255de6-d4c5-4b2a-b27c-afae8d42a1b4
📒 Files selected for processing (14)
Backend/src/ShoeStore.Api/Controllers/UserVoucherController.csBackend/src/ShoeStore.Api/Controllers/VoucherController.csBackend/src/ShoeStore.Application/DTOs/VoucherDtos/DeleteVoucherDto.csBackend/src/ShoeStore.Application/DTOs/VoucherDtos/ResponseVoucherAdminDto.csBackend/src/ShoeStore.Application/DTOs/VoucherDtos/ResponseVoucherUserDto.csBackend/src/ShoeStore.Application/Interface/UserInterface/IUserRepository.csBackend/src/ShoeStore.Application/Interface/VoucherInterface/IUserVoucherRepository.csBackend/src/ShoeStore.Application/Interface/VoucherInterface/IUserVoucherService.csBackend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.csBackend/src/ShoeStore.Application/Services/VoucherService.csBackend/src/ShoeStore.Infrastructure/DependencyInjection/DependencyInjection.csBackend/src/ShoeStore.Infrastructure/Repositories/UserRepository.csBackend/src/ShoeStore.Infrastructure/Repositories/UserVoucherRepository.csBackend/src/ShoeStore.Infrastructure/Repositories/VoucherRepository.cs
✅ Files skipped from review due to trivial changes (2)
- Backend/src/ShoeStore.Application/DTOs/VoucherDtos/DeleteVoucherDto.cs
- Backend/src/ShoeStore.Application/DTOs/VoucherDtos/ResponseVoucherAdminDto.cs
🚧 Files skipped from review as they are similar to previous changes (2)
- Backend/src/ShoeStore.Infrastructure/DependencyInjection/DependencyInjection.cs
- Backend/src/ShoeStore.Infrastructure/Repositories/VoucherRepository.cs
| return await result.MatchAsync<IActionResult>( | ||
| async _ => | ||
| { | ||
| var adminEmail = User.FindFirstValue(ClaimTypes.Email) ?? "[email protected]"; | ||
|
|
||
| await voucherService.NotifyUserAboutNewVoucherAsync( | ||
| adminEmail: adminEmail, | ||
| voucherName: createVoucherDto.VoucherName ?? "New Discount", | ||
| validTo: createVoucherDto.ValidTo ?? DateTime.UtcNow, | ||
| token: token | ||
| ); | ||
|
|
||
| return Created("", new { message = "Voucher created and users notified" }); | ||
| }, | ||
| errors => Task.FromResult<IActionResult>(BadRequest(new | ||
| { | ||
| message = "Failed to create voucher", | ||
| detail = errors[0].Description | ||
| })) | ||
| ); | ||
| } |
There was a problem hiding this comment.
Notification failure swallows successful voucher creation and returns 400.
MatchAsync only enters the success branch after CreateVoucherAsync has already persisted the voucher (per VoucherService.CreateVoucherAsync calling uow.SaveChangesAsync before returning Result.Created). If NotifyUserAboutNewVoucherAsync then returns an ErrorOr failure (e.g., SMTP failure → EMAIL_SENDING_FAILED), its result is silently ignored here — the awaited Task<ErrorOr<Success>> is discarded — so the API still responds 201 Created. Conversely, if the notify call throws an exception, the whole request bubbles up as 500, even though the voucher was committed. Either way, callers cannot reliably distinguish "voucher created" from "voucher created + notification failed".
Consider:
- Inspecting the result of
NotifyUserAboutNewVoucherAsyncand surfacing a partial-success response (e.g.,201with a warning), or - Moving notification dispatch out-of-band (background job / outbox) so HTTP success reflects only the persisted state.
Additionally, Created("", ...) emits an empty Location header; prefer CreatedAtAction(nameof(GetVouchersForAdmin), ...) or returning the new voucher's GUID-based URL.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Backend/src/ShoeStore.Api/Controllers/VoucherController.cs` around lines 49 -
69, The success branch of result.MatchAsync ignores the outcome of
NotifyUserAboutNewVoucherAsync causing notification failures to be swallowed or
to surface as 500 even though CreateVoucherAsync already persisted the voucher;
update the success branch to await and inspect the ErrorOr result from
NotifyUserAboutNewVoucherAsync (handle both Error and Success cases), return
CreatedAtAction(nameof(GetVouchersForAdmin), ...) or CreatedAtAction with the
new voucher id and include a partial-success warning when notification failed,
and ensure any exceptions from NotifyUserAboutNewVoucherAsync are caught and
translated into a non-failing warning response (or, alternatively, move
notification dispatch out-of-band via an outbox/background job) so HTTP 201
reflects the persisted voucher state rather than the delivery outcome.
| return await result.MatchAsync<IActionResult>( | ||
| async _ => | ||
| { | ||
| var adminEmail = User.FindFirstValue(ClaimTypes.Email) ?? "[email protected]"; |
There was a problem hiding this comment.
Hardcoded [email protected] fallback is a smell.
If the authenticated principal lacks an Email claim, the email will be sent from: "[email protected]", which is RFC 2606 reserved and will be rejected or flagged as spam by most SMTP relays, and may also leak into outgoing email headers. Prefer failing fast with a 400/401 (Admin must have an email claim) or sourcing the sender from configuration rather than hardcoding a placeholder address into the request flow.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Backend/src/ShoeStore.Api/Controllers/VoucherController.cs` at line 52, The
current fallback of var adminEmail = User.FindFirstValue(ClaimTypes.Email) ??
"[email protected]" in VoucherController is unsafe; replace this by validating
the presence of the Email claim and returning an appropriate error (e.g.,
400/401) if missing, or read a configured sender address instead of a hardcoded
placeholder. Locate the adminEmail assignment in the VoucherController, remove
the "[email protected]" default, and implement either: (a) a guard that checks
User.FindFirstValue(ClaimTypes.Email) and returns BadRequest/Unauthorized with a
clear message if null/empty, or (b) resolve the sender from configuration
(IConfiguration or options) and use that when the claim is absent, ensuring no
hardcoded reserved address is ever used.
| [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] | ||
| [ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)] | ||
| [ProducesResponseType(typeof(object), StatusCodes.Status401Unauthorized)] | ||
| [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] | ||
| [HttpDelete("expire")] | ||
| public async Task<IActionResult> DeleteExpiredVouchers(CancellationToken token) | ||
| { | ||
| var result = await voucherService.DeleteVoucherExpireAsync(token); | ||
| return result.Match<IActionResult>( | ||
| _ => Ok(new { message = "Expired vouchers deleted successfully" }), | ||
| errors => BadRequest(new | ||
| { | ||
| message = "Failed to delete expired vouchers", | ||
| detail = errors[0].Description | ||
| })); | ||
| } |
There was a problem hiding this comment.
NO_EXPIRED_VOUCHERS is mapped to 400 instead of 404.
VoucherService.DeleteVoucherExpireAsync returns Error.NotFound("NO_EXPIRED_VOUCHERS", ...) (see lines 85–88 of VoucherService.cs), but this handler funnels every error into BadRequest, and the [ProducesResponseType] attributes don't advertise 404 either. This is inconsistent with the other handlers (UpdateVoucher, DeleteVoucher, GetVouchersForAdmin, GetAllVouchers) which switch on errors[0].Code to surface NotFound.
Proposed fix
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(object), StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
[HttpDelete("expire")]
public async Task<IActionResult> DeleteExpiredVouchers(CancellationToken token)
{
var result = await voucherService.DeleteVoucherExpireAsync(token);
return result.Match<IActionResult>(
_ => Ok(new { message = "Expired vouchers deleted successfully" }),
- errors => BadRequest(new
+ errors => errors[0].Code switch
{
- message = "Failed to delete expired vouchers",
- detail = errors[0].Description
- }));
+ "NO_EXPIRED_VOUCHERS" => NotFound(new
+ {
+ message = "No expired vouchers found",
+ detail = errors[0].Description
+ }),
+ _ => BadRequest(new
+ {
+ message = "Failed to delete expired vouchers",
+ detail = errors[0].Description
+ })
+ });
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Backend/src/ShoeStore.Api/Controllers/VoucherController.cs` around lines 166
- 181, The DeleteExpiredVouchers handler currently maps all errors to
BadRequest; change it to inspect the returned errors (from
voucherService.DeleteVoucherExpireAsync) and when errors[0].Code ==
"NO_EXPIRED_VOUCHERS" return NotFound(...) instead of BadRequest, mirroring
other handlers (e.g.,
UpdateVoucher/DeleteVoucher/GetVouchersForAdmin/GetAllVouchers) and add a
corresponding [ProducesResponseType(typeof(object),
StatusCodes.Status404NotFound)] attribute to the DeleteExpiredVouchers action;
ensure other error cases still produce BadRequest as before.
| private DateTime? _validFrom; | ||
| public DateTime? ValidFrom | ||
| { | ||
| get => _validFrom; | ||
| set => _validFrom = value.HasValue ? DateTime.SpecifyKind(value.Value, DateTimeKind.Utc) : null; | ||
| } | ||
|
|
||
| private DateTime? _validTo; | ||
| public DateTime? ValidTo | ||
| { | ||
| get => _validTo; | ||
| set => _validTo = value.HasValue ? DateTime.SpecifyKind(value.Value, DateTimeKind.Utc) : null; | ||
| } |
There was a problem hiding this comment.
SpecifyKind relabels rather than converts — silent time-zone corruption risk.
DateTime.SpecifyKind does not convert the value; it only changes the Kind flag. If a caller ever assigns a DateTime whose Kind is Local or Unspecified but represents local/zoned time, the setter will silently mark it as UTC without adjusting the underlying value, leading to incorrect ValidFrom/ValidTo returned to users.
This is currently safe only because the entity values are assumed to already be in UTC. Consider ToUniversalTime() (or asserting the Kind) if there's any chance the source isn't UTC.
♻️ Suggested adjustment
- set => _validFrom = value.HasValue ? DateTime.SpecifyKind(value.Value, DateTimeKind.Utc) : null;
+ set => _validFrom = value.HasValue
+ ? (value.Value.Kind == DateTimeKind.Utc
+ ? value.Value
+ : value.Value.ToUniversalTime())
+ : null;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Backend/src/ShoeStore.Application/DTOs/VoucherDtos/ResponseVoucherUserDto.cs`
around lines 9 - 21, The setters for ValidFrom/ValidTo use DateTime.SpecifyKind
which only relabels Kind and can silently corrupt times; update the setters for
_validFrom and _validTo to ensure the incoming DateTime is converted to UTC
instead of merely changing its Kind — e.g., if value.HasValue, convert
value.Value to UTC (using ToUniversalTime or by checking Kind and calling
ToUniversalTime when Kind != Utc) and then store the converted UTC DateTime;
replace the use of DateTime.SpecifyKind with a proper UTC conversion flow for
both ValidFrom and ValidTo.
| { | ||
| public interface IUserVoucherService : IVoucherService | ||
| { | ||
| Task<ErrorOr<PageResult<ResponseVoucherUserDto>>> GetAllVoucherForUserAsync(Guid UserGuid, CancellationToken token); |
There was a problem hiding this comment.
Parameter naming convention: use camelCase.
Guid UserGuid should be Guid userGuid to follow standard C# parameter naming conventions (the rest of the codebase, including GetVouchersByUserGuid(Guid userGuid), already uses camelCase).
- Task<ErrorOr<PageResult<ResponseVoucherUserDto>>> GetAllVoucherForUserAsync(Guid UserGuid, CancellationToken token);
+ Task<ErrorOr<PageResult<ResponseVoucherUserDto>>> GetAllVoucherForUserAsync(Guid userGuid, CancellationToken token);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Task<ErrorOr<PageResult<ResponseVoucherUserDto>>> GetAllVoucherForUserAsync(Guid UserGuid, CancellationToken token); | |
| Task<ErrorOr<PageResult<ResponseVoucherUserDto>>> GetAllVoucherForUserAsync(Guid userGuid, CancellationToken token); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@Backend/src/ShoeStore.Application/Interface/VoucherInterface/IUserVoucherService.cs`
at line 8, Rename the parameter in the method signature
GetAllVoucherForUserAsync from Guid UserGuid to Guid userGuid to follow C#
camelCase conventions and match other APIs (e.g., GetVouchersByUserGuid); update
any implementations and callers of IUserVoucherService.GetAllVoucherForUserAsync
to use the new parameter name to keep signatures consistent.
| public async Task<ErrorOr<PageResult<ResponseVoucherUserDto>>> GetAllVoucherForUserAsync(Guid UserGuid, CancellationToken token) | ||
| { | ||
| var vouchers = await userVoucherRepository | ||
| .GetVouchersByUserGuid(UserGuid) | ||
| .Where(v => !v.Voucher.IsDeleted && v.Voucher.ValidTo > DateTime.UtcNow) | ||
| .Select(v => new ResponseVoucherUserDto | ||
| { | ||
| VoucherName = v.Voucher.VoucherName ?? string.Empty, | ||
| Description = v.Voucher.VoucherDescription ?? string.Empty, | ||
| Discount = v.Voucher.Discount, | ||
| ValidFrom = v.Voucher.ValidFrom, | ||
| ValidTo = v.Voucher.ValidTo | ||
| }) | ||
| .ToListAsync(token); | ||
|
|
||
| if(!vouchers.Any()) | ||
| { | ||
| return Error.NotFound( | ||
| "NO_VOUCHERS_FOUND", | ||
| "No vouchers were found for the user." | ||
| ); | ||
| } | ||
|
|
||
| var result = new PageResult<ResponseVoucherUserDto> | ||
| { | ||
| Items = vouchers, | ||
| TotalCount = vouchers.Count | ||
| }; | ||
| return result; | ||
| } |
There was a problem hiding this comment.
Possible NullReferenceException on v.Voucher and missing tracking semantics.
Static analysis flags v.Voucher.IsDeleted / v.Voucher.VoucherName as dereferences of a possibly null reference. Per UserVoucherRepository.GetVouchersByUserGuid, the query is context.UserVouchers.Where(uv => uv.User != null && uv.User.PublicId == userGuid) — there is no Include/Where guarding Voucher, so a UserVoucher with a null Voucher navigation will NRE either at filter time (translated SQL is fine, but client evaluation is not) or after materialization. Add && v.Voucher != null to the predicate (or filter at the repository level) to satisfy the analyzer and harden the query against orphan rows.
🧰 Tools
🪛 GitHub Check: Backend Build and Analyze
[warning] 107-107:
Dereference of a possibly null reference.
[warning] 104-104:
Dereference of a possibly null reference.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Backend/src/ShoeStore.Application/Services/VoucherService.cs` around lines
100 - 129, GetAllVoucherForUserAsync can NRE on v.Voucher; update the query from
userVoucherRepository.GetVouchersByUserGuid(...) so the predicate guarantees
Voucher is present (e.g., add && v.Voucher != null in the Where chain or ensure
the repository includes/filters Voucher) before dereferencing v.Voucher fields,
then keep the existing projection to ResponseVoucherUserDto; reference the
GetAllVoucherForUserAsync method and the
userVoucherRepository.GetVouchersByUserGuid(...) call and the v.Voucher usages
when applying the change.
| ValidTo = v.ValidTo, | ||
| MinOrderPrice = v.MinOrderPrice | ||
| }) | ||
| .ToListAsync(); |
There was a problem hiding this comment.
Missing CancellationToken on ToListAsync in GetVoucherForAdminAsync.
Unlike GetAllVouchersAsync (line 147), this overload is called without the token, so the read cannot be cancelled when the client disconnects.
- .ToListAsync();
+ .ToListAsync(token);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .ToListAsync(); | |
| .ToListAsync(token); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Backend/src/ShoeStore.Application/Services/VoucherService.cs` at line 181,
GetVoucherForAdminAsync calls ToListAsync() without the CancellationToken so the
database read can't be cancelled; update the ToListAsync call in
GetVoucherForAdminAsync to pass the method's cancellationToken (e.g., call
ToListAsync(cancellationToken)) matching the pattern used in
GetAllVouchersAsync, ensuring the async query observes client cancellation.
| public async Task<ErrorOr<Success>> NotifyUserAboutNewVoucherAsync(string adminEmail, string voucherName, DateTime validTo, CancellationToken token) | ||
| { | ||
| try | ||
| { | ||
| var users = await userRepository | ||
| .GetAllUsers() | ||
| .Where(u => u.Email != adminEmail) | ||
| .ToListAsync(token); | ||
|
|
||
| foreach (var user in users) | ||
| { | ||
| if (user.Email != adminEmail) | ||
| { | ||
| return Error.NotFound( | ||
| "USER_NOT_FOUND", | ||
| "The user with the specified email does not exist." | ||
| ); | ||
| } | ||
| string emailBody = $@" | ||
| Hi {user.UserName}, | ||
|
|
||
| Great news! A new voucher has been added to your account: | ||
|
|
||
| 🎁 {voucherName.ToUpper()} | ||
| 📅 Valid until: {validTo:MMMM dd, yyyy} | ||
|
|
||
| Check your wallet and start shopping now to enjoy your discount! | ||
|
|
||
| Best regards, | ||
| Shoe Store Team"; | ||
|
|
||
| await emailService.SendEmailAsync( | ||
| from: adminEmail, | ||
| to: user.Email, | ||
| subject: "🎁 New Voucher Received!", | ||
| body: emailBody, | ||
| token: token | ||
| ); | ||
| } | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| return Error.Failure( | ||
| "EMAIL_SENDING_FAILED", | ||
| $"Failed to send email notifications: {ex.Message}" | ||
| ); | ||
| } | ||
| return Result.Success; | ||
| } |
There was a problem hiding this comment.
Critical logic bug: notification loop returns USER_NOT_FOUND on the first iteration and sends zero emails.
The query on line 204 already filters u => u.Email != adminEmail, so by construction every user in users satisfies user.Email != adminEmail. The check at lines 209–215 then evaluates to true for the very first user, returns Error.NotFound("USER_NOT_FOUND", ...) and exits the method — no email is ever sent, and the controller will surface this as a 400 to the caller.
The condition is inverted/redundant and should be removed entirely; the Where already enforces the intended exclusion.
Proposed fix
var users = await userRepository
.GetAllUsers()
- .Where(u => u.Email != adminEmail)
+ .Where(u => u.Email != null && u.Email != adminEmail)
.ToListAsync(token);
foreach (var user in users)
{
- if (user.Email != adminEmail)
- {
- return Error.NotFound(
- "USER_NOT_FOUND",
- "The user with the specified email does not exist."
- );
- }
string emailBody = $@"
Hi {user.UserName},Additionally, consider:
- Email comparison should be case-insensitive (
StringComparer.OrdinalIgnoreCase/ DB collation), since[email protected]and[email protected]are the same mailbox. - The current loop sends one email per user serially inside an HTTP request path; for any non-trivial user base this will time out. Consider dispatching to a background worker or using
Task.WhenAllwith a bounded concurrency limit. - Per-user send failures abort the entire batch and lose progress; collect and report partial failures rather than throwing on the first one.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Backend/src/ShoeStore.Application/Services/VoucherService.cs` around lines
198 - 246, The loop in NotifyUserAboutNewVoucherAsync currently returns
Error.NotFound because of a redundant/inverted check against user.Email after
fetching users with userRepository.GetAllUsers().Where(u => u.Email !=
adminEmail); remove the entire if (user.Email != adminEmail) { return
Error.NotFound(...); } block, change the admin-email exclusion to a
case-insensitive comparison (e.g., compare using
StringComparer.OrdinalIgnoreCase or ensure the DB collation) when building the
query, and replace the serial await emailService.SendEmailAsync calls with a
strategy that supports concurrency and partial-failure reporting (e.g.,
Task.WhenAll with a bounded concurrency limiter or enqueue jobs to a background
worker) so per-user send failures are collected and returned as aggregated
errors instead of aborting the whole batch.
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
|



Summary by CodeRabbit